﻿/*
    class=data-model
        subscription
        bindings
            binding
                element
                handle
*/

(function (linq, $, globalize, undef) {
    var randomId = new Date().getTime();
    var extendedDataProperty = '__ex' + randomId;
    var metadataTypeProperty = '__metadata' + randomId;
    var observables = [];
    var timerId;
    var timerDelay = 10;
    var bindingProviders = {};
    var settings = {};
    var types = {};
    var converters = {
        register: function (name, converter) {
            if (name == 'register') return;
            converters[name] = linq.utils.wrap(converter);
            return this;
        }
    };

    $.each({
        pages: function (value, items) {
            if (typeof value == 'number') {
                value = { count: value, current: 0 };
            }
            if (items == null) items = 5;
            var array = linq(value.count).skip(Math.min(value.count - items, value.current - Math.floor(items / 2))).take(items).array();
            array.count = value.count;
            array.current = value.current;
            return array;
        },
        nums: function (value) {
            value = value == null ? '' : value.toString().replace(/\s+/g, '');
            var result = [];
            if (value) {
                $.each(value.split(','), function (index, item) {
                    result[index] = parseFloat(item);
                });
            }
            return result;
        },
        ints: function (value) {
            value = value == null ? '' : value.toString().replace(/\s+/g, '');
            var result = [];
            if (value) {
                $.each(value.split(','), function (index, item) {
                    result[index] = parseInt(item, 10);
                });
            }
            return result;
        },
        hexes: function (value) {
            value = value == null ? '' : value.toString().replace(/\s+/g, '');
            var result = [];
            if (value) {
                $.each(value.split(','), function (index, item) {
                    result[index] = parseInt(item, 16);
                });
            }
            return result;
        }
    }, function (name, converter) {
        converters.register(name, converter);
    });

    var serializers = {
        number: String,
        boolean: String,
        string: function (value, format) {
            if (value == null) return '';
            if (globalize) return globalize.format(value, format || (value instanceof Date ? 'dd-MM-yyyy HH:mm:ss' : null));
            return String(value);
        },
        date: String,
        array: function (value) {
            if ($.isArray(value)) return value.join(', ');
            return String(value);
        }
    };
    var deserializers = {
        number: Number,
        string: String,
        date: Date,
        boolean: Boolean,
        array: function (value) {
            if ($.isArray(value)) return value;
            value = trim(value == null ? '' : value.toString());
            if (!value || value == ',') return [];
            return value.split(/\s*,\s*/);
        }
    };

    function compareObject(a, b, exact) {
        if (a != null && b != null && a.__type && (a.__type === b.__type)) {
            var type = types[a.__type];
            if (type) {
                if (type.compare) return type.compare(a, b);
                if (type.getHashCode) return type.getHashCode(a) === type.getHashCode(b);
                if (type.properties) {
                    var equal = true;
                    var comparer = exact ? exactComparer : settings.comparer;
                    $.each(function (index, property) {
                        if (!comparer(a[property], b[property])) {
                            equal = false;
                            return false;
                        }
                    });
                    return equal;
                }
            }
        }
        return false;
    }

    function defaultComparer(a, b) {
        return compareObject(a, b) || (a == b) || (typeof a == 'number' && typeof b == 'number' && isNaN(a) && isNaN(b));
    }

    function exactComparer(a, b) {
        return compareObject(a, b, true) || (a === b) || (typeof a == 'number' && typeof b == 'number' && isNaN(a) && isNaN(b));
    }

    function getData(obj, createNew) {
        var ex = obj[extendedDataProperty];
        if (!ex && createNew) {
            obj[extendedDataProperty] = ex = {};
            if (createNew instanceof Function) return createNew(ex);
            if (typeof createNew == 'string') return ex[createNew];
        }
        return ex;
    }

    function removeData(obj) {
        delete obj[extendedDataProperty];
    }

    function isObservable(obj) {
        if (!obj) return false;
        var ex = getData(obj);
        return ex && ex.subscribable;
    }

    function removeNode(node) {
        if (!node) return;
        node = $(node);
        var modelElements = node.find('.data-model').toArray();
        if (node.is('.data-model')) modelElements.push(node);
        $.each(modelElements, function (index, modelElement) {
            var observableData = $(modelElement).data('observable-data');
            if (observableData.bindings) {
                $.each(observableData.bindings, function (i, binding) {
                    $(binding.element).data('removed', true);
                });
            }
            if (observableData.model) {
                if (observableData) {
                    if (observableData.subscription) {
                        var observable = linq.model(observableData.model);
                        observable.unsubscribe(observableData.subscription);
                        delete observableData.subscription;
                    }
                }
                delete observableData.model;
            }

            $(modelElement).removeData('observable-data');
        });
        $(node).remove();
    }

    function changeInfo(property, current, old, extendData) {
        this.property = property;
        this.current = current;
        this.old = old;
        if (extendData) $.extend(this, extendData);
    }

    changeInfo.prototype = {
        change: isPropertyChange
    };

    function objectChangingDetector() {
        var model = this.model;
        var old = this.old;
        var changeSet = [];
        var comparer = this.comparer || settings.comparer;
        for (var i in model) {
            var newValue = model[i];
            if (typeof newValue == 'function') continue;
            var oldValue = old[i];
            if (!comparer(oldValue, newValue)) {
                changeSet.push(new changeInfo(i, newValue, oldValue));
                old[i] = newValue;
            }
        }
        return changeSet;
    }

    function arrayChangingDetector() {
        var ex = getData(this.model);
        if (!ex || !ex.changeSet) return [];
        var result = ex.changeSet.slice(0);
        ex.changeSet = [];
        if (!result.length && this.old.length != this.model.length) {
            result.push(new changeInfo('*', null, null, { items: this.model.slice(0) }));
            this.old = this.model.slice(0);
        }
        return result;
    }

    function addToChangeSet(obj, change) {
        var ex = getData(obj, true);
        if (!ex.changeSet) ex.changeSet = [];
        ex.changeSet.push(change);
    }

    function compareArray(array1, array2, exact) {
        if (array1.length != array2.length) return true;
        var comparer = typeof exact == 'function' ? exact : exact ? exactComparer : settings.comparer;
        var length = array1.length;
        for (var i = 0; i < length; i++) {
            if (!comparer(array1[i], array2[i])) return true;
        }
        return false;
    }

    function registerObservable(model) {
        var ex = getData(model, true);
        if (ex.subscribable) return ex.subscribable;
        var result;

        if ($.isArray(model)) {
            observables.push(result = { old: model.slice(0), model: model, getChangeSet: arrayChangingDetector });
        }
        else {
            var old = {};
            for (var property in model) {
                var value = model[property];
                if (value instanceof Function) continue;
                old[property] = value;
            }
            observables.push(result = { old: old, model: model, getChangeSet: objectChangingDetector });
        }

        ex.subscribable = result;
        return result;
    }

    function unregisterObserable(model) {
        var ex = getData(model);
        if (!ex || !ex.subscribable) return;
        for (var i = 0; i < observables.length; i++) {
            var o = observables[i];
            if (o.target === model) {
                observables.splice(i, 1);
                delete ex.subscribable;
                return;
            }
        }
    }

    function createMetadata(model) {
        var metadata = {};
        $.each(model, function (name, value) {
            if (name == extendedDataProperty) return;
            var property;
            if (typeof value == 'function') {
                property = function () {
                    return value.apply(model, arguments);
                };
            }
            else {
                property = {};
            }
            property.model = model;
            property.value = value;
            property[metadataTypeProperty] = true;
            property.property = name;
            property.valueOf = function () {
                return this.value;
            };
            metadata[name] = property;
        });
        return metadata;
    }

    function bindingUtility() {

    }

    $.extend(bindingUtility.prototype, {
        guidEmpty: function (value) {
            if (arguments.length == 0) return '00000000-0000-0000-0000-000000000000';
            if (value) {
                if (value[metadataTypeProperty]) value = value.value;
                return value === '00000000-0000-0000-0000-000000000000';
            }
            return false;
        }
    });

    function bindingEventArgs(binding, data, provider) {
        var isArray = $.isArray(data);
        var isNull = data == null;
        this.provider = provider;
        this.params = isNull ? {} : data;
        this.args = isNull ? [] : isArray ? data : [];
        this.data = data;
        this.model = binding.model;
        this.element = binding.element;
        this.getMetadata = function () {
            if (arguments.length == 0) return this.getMetadata(this.data);
            var first = null;
            for (var i = 0; i < arguments.length; i++) {
                var value = arguments[i];
                if (value != null && value[metadataTypeProperty]) return value;
                if (value != null && first == null) first = value;
            }
            return { value: first, invalid: true, valueOf: function () { return this.value; } };
        };
        this.lock = function (flag) {
            if (flag) binding.lock = true;
        };
        this.unlock = function (callback) {
            if (!binding.lock) callback.call(binding.element);
            binding.lock = false;
        };
        this.cache = function (name, value) {
            if (!binding.data) binding.data = {};
            var cache = binding.data[provider];
            if (!cache) binding.data[provider] = cache = {};
            if ($.isPlainObject(name)) {
                $.extend(cache, name);
                return this;
            }
            if (arguments.length == 0) return cache;
            if (arguments.length == 1) return cache[name];
            cache[name] = value;
            return this;
        };
    }

    function isPropertyChange(property, callback, propertyCanNull) {
        var context = this.element || this.model;
        var currentProperty = this.property;
        if ($.isPlainObject(arguments[0])) {
            propertyCanNull = arguments[1];
            $.each(arguments[0], function (name, value) {
                if (name == '*' || currentProperty == '*' || name == currentProperty || (propertyCanNull && !name)) {
                    if (value) value.call(context);
                    if (currentProperty != '*' || name == '*') return false;
                }
            });
            return this;
        }
        else {
            if (typeof callback !== 'function') {
                propertyCanNull = callback;
                callback = null;
            }
            var result = property == '*' || this.property == '*' || property == this.property || (property instanceof RegExp && property.test(property)) || (propertyCanNull && !property);
            if (result && callback) callback.call(context);
            return result;
        }
    }

    function isElementRemoved(element) {
        return !!$(element).data('removed');
    }

    bindingEventArgs.prototype = {
        change: isPropertyChange,
        removed: function () {
            return isElementRemoved(this.element);
        }
    };

    function createBindingSubscription(model, bindings) {
        return function (property, newValue, notifyInfo) {
            $.each(bindings, function (index, binding) {
                var providers = evalBindingProviders(binding);
                var updates = [];
                var global = {};
                $.each(providers, function (name, value) {
                    var provider = bindingProviders[name];

                    if (provider) {
                        var params = new bindingEventArgs(binding, value, name);
                        params.property = property;
                        params.notifyInfo = notifyInfo;
                        params.newValue = newValue;
                        params.all = global;
                        if (typeof provider == 'function') provider = { update: provider };
                        if (provider.update) updates.push({ handle: provider.update, params: params });
                    }
                    else {
                        global[name] = value;
                    }
                });
                $.each(updates, function (i, provider) {
                    provider.handle.call(binding.element, provider.params);
                });
            });
        };
    }

    function evalBindingProviders(binding) {
        return binding.handle.call(
            binding.element,
            binding.model,
            createMetadata(binding.model),
            new myObject(binding.element),
            new bindingUtility(binding)
            );
    }

    function applyBinding(binding) {
        var providers = evalBindingProviders(binding);
        var inits = [];
        var updates = [];
        var global = {};
        var initialized = binding.init;
        var observableData = binding.subscribable;
        if (!observableData.subscription) {
            observableData.bindings = [];
            observableData.subscription = createBindingSubscription(observableData.model, observableData.bindings);
            observableData.subscribable.subscribe(observableData.subscription);
        }
        observableData.bindings.push(binding);
        $.each(providers, function (name, value) {
            var provider = bindingProviders[name];

            if (provider) {
                var params = new bindingEventArgs(binding, value, name);
                params.property = '*';
                params.all = global;
                if (typeof provider == 'function') {
                    provider = { update: provider };
                }
                if (!initialized && provider.init) inits.push({ handle: provider.init, params: params });
                if (provider.update) updates.push({ handle: provider.update, params: params });
            }
            else {
                global[name] = value;
            }
        });
        if (!initialized) {
            binding.init = true;
            if (inits.length) {
                $.each(inits, function (index, provider) {
                    provider.handle.call(binding.element, provider.params);
                });
            }
        }
        $.each(updates, function (index, provider) {
            provider.handle.call(binding.element, provider.params);
        });
    }

    function clone(model, removeMethod) {
        var isArray = $.isArray(model);
        if (isArray || $.isPlainObject(model)) {
            var newModel = isArray ? [] : {};
            $.each(model, function (name, value) {
                if (name === extendedDataProperty) return;
                if (value) {
                    if (value[metadataTypeProperty]) value = value.value;
                    if (value && $.isArray(value) || $.isPlainObject(value)) {
                        value = clone(value, removeMethod);
                    }
                }
                if (removeMethod && typeof value == 'function') {
                    if (!isArray) return;
                    value = null;
                }
                newModel[name] = value;
            });
            return newModel;
        }
        return model;
    }

    function trim(value) {
        return (value || '').replace(/^\s+|\s+$/g, '');
    }

    function myObject(element) {
        var cache = {};
        this.parent = function (level) {
            level = parseInt(level, 10) || 0;
            if (level <= 0) level = 1;
            var lastParent = cache[level];
            if (!lastParent && !cache.hasOwnProperty(level)) {
                var current = $(element);
                var i = 0;
                while (i < level) {
                    current = current.closest('.data-model');
                    if (!current.length) break;
                    cache[i] = lastParent = current;
                    current = current.parent();
                    i++;
                }
                cache[level] = lastParent;
            }
            if (!lastParent || !lastParent.length) return null;
            var data = lastParent.data('observable-data');
            if (!data) return null;
            return data.model;
        };
        this.root = function () {
            return this.parent(9999);
        };
    }

    function getTemplate(templateSelector, container) {
        var node = null;
        if (templateSelector) {
            node = $(templateSelector).get(0);
        }
        else {
            if (container) {
                var templateId = $(container).attr('inline-template-id');
                if (templateId) node = $('#' + templateId).get(0);
            }
        }
        var bindable = node ? $(node).attr('bindable') != 'false' : false;
        return { template: node, bindable: bindable };
    }

    linq.model = function (model, selector) {
        if (model && model[metadataTypeProperty]) model = model.value;
        if (!$.isArray(model) && !$.isPlainObject(model)) return model;
        if (model && model.subscribe && model.unsubscribe) return model;
        var observable = registerObservable(model);
        var subscribable = {
            subscribe: function (subscription, condition, context) {
                if (subscription) {
                    var subscriptions = observable.subscriptions || (observable.subscriptions = []);
                    for (var i = 0; i < subscriptions.length; i++) {
                        var data = subscriptions[i];
                        if (data.callback == subscription) {
                            if (arguments.length > 1) data.condition = condition === false ? null : condition || data.condition;
                            if (arguments.length > 2) data.context = context === false ? null : context || data.context;
                            return this;
                        }
                    }
                    subscriptions.push({ callback: subscription, condition: condition, context: context });
                }
                return this;
            },
            unsubscribe: function (subscription) {
                if (subscription) {
                    if (observable.subscriptions) {
                        var subscriptions = observable.subscriptions;
                        for (var i = 0; i < subscriptions.length; i++) {
                            var data = subscriptions[i];
                            if (data.callback == subscription) {
                                subscriptions.splice(i, 1);
                                break;
                            }
                        }
                    }
                }
                return this;
            },
            refresh: function (recursive) {
                var obs = observables.slice(0);
                var refreshInternal = function (m, r, isModel) {
                    if (!isModel) {
                        if (m && m[metadataTypeProperty]) m = m.value;
                        if (!$.isArray(m) && !$.isPlainObject(m)) return;
                        if (m && m.subscribe && m.unsubscribe) return;
                    }
                    for (var i = 0; i < obs.length; i++) {
                        var ob = obs[i];
                        if (ob.model !== m) continue;
                        var subscriptions = ob.subscriptions;
                        if (!subscriptions || !subscriptions.length) continue;
                        subscribe([new changeInfo('*')], ob, subscriptions);
                        break;
                    }
                    if (recursive) {
                        $(m, function (name, value) {
                            refreshInternal(value, r, false);
                        });
                    }
                };
                refreshInternal(model, recursive, true);
            },
            clone: function () {
                return clone(model, true);
            },
            detach: function () {
                unregisterObserable(model);
                return model;
            }
        };
        if (selector) {
            selector = $(selector);
            selector.find('.ui-inline-template').each(function () {
                var jq = $(this);
                var innerScript = jq.find('script:only-child');
                var id;
                if (innerScript.length) {
                    id = innerScript.attr('id');
                    if (!id) {
                        id = linq.id('ui-inline-template-');
                        innerScript.attr('id', id);
                    }
                    innerScript.prependTo(document.body);
                }
                else {
                    id = linq.id('ui-inline-template-');
                    innerScript = $('<xmp type="text/html" style="display: none;"></xmp>');
                    innerScript.text(jq.html());
                    innerScript.attr('bindable', jq.attr('bindable'));
                    innerScript.attr('id', id);
                    $(document.body).prepend(innerScript);
                }

                jq.attr('inline-template-id', id).empty();
            });
            var bindingElements = selector.filter('[data-bind]').toArray().concat(selector.find('[data-bind]').toArray());
            var bindings = [];
            var observableData = { subscribable: subscribable, model: model };
            selector.data('observable-data', observableData).addClass('data-model');
            $.each(bindingElements, function (index, bindingElement) {
                var bindingHandle = new Function('model, metadata, my, util', 'var $model = model, $my = my, $meta = metadata, $converters = linq.model.converters, $serializers = linq.model.serializers, $deserializers = linq.model.deserializers, $util = util; with(metadata) {return {' + $(bindingElement).attr('data-bind') + '};}');
                var binding = {
                    element: bindingElement,
                    handle: bindingHandle,
                    model: model,
                    subscribable: observableData
                };
                $(bindingElement).data('binding', binding);
                bindings.push(binding);
                applyBinding(binding);
            });
        }

        return subscribable;
    };

    var arrayProto = Array.prototype;
    $.each(['push', 'pop', 'shift', 'unshift'], function (i, method) {
        var oldMethod = arrayProto[method];
        arrayProto[method] = function () {
            if (!isObservable(this)) return oldMethod.apply(this, arguments);
            var oldData = this.slice(0);
            var result = oldMethod.apply(this, arguments);
            if (oldData.length != this.length) {
                var items = [];
                switch (method) {
                    case 'push':
                        items = this.slice(oldData.length);
                        break;
                    case 'pop':
                        items = oldData.slice(this.length);
                        break;
                    case 'shift':
                        items = oldData.slice(0, oldData.length - this.length);
                        break;
                    case 'unshift':
                        items = this.slice(0, this.length - oldData.length);
                        break;
                }
                addToChangeSet(this, new changeInfo(method, null, null, { args: arguments, result: result, items: items }));
            }
            return result;
        };
    });

    $.each(['sort', 'reverse'], function (index, method) {
        var oldMethod = arrayProto[method];
        arrayProto[method] = function () {
            if (!this.length || !isObservable(this)) return oldMethod.apply(this, arguments);
            if (this.length > 1) {
                var oldData = this.slice(0);
                var result = oldMethod.apply(this, arguments);
                if (compareArray(oldData, this, true)) addToChangeSet(this, new changeInfo(method, null, null, { args: arguments, result: result, items: this.slice(0) }));
                return result;
            }
            else {
                return oldMethod.apply(this, arguments);
            }
        };
    });

    $.each(['splice'], function (index, method) {
        var oldMethod = arrayProto[method];
        arrayProto[method] = function () {
            if (!isObservable(this)) return oldMethod.apply(this, arguments);
            var oldData = this.slice(0);
            var result = oldMethod.apply(this, arguments);
            if (oldData.length != this.length || compareArray(oldData, this, true)) addToChangeSet(this, new changeInfo(method, { args: arguments, result: result, items: this.slice(0) }));
        };
    });

    // date,string,number,array,boolean
    function getSerializer(value, serializer) {
        if (serializer) {
            if (typeof serializer == 'string') {
                if (serializer.charAt(0) == '#')
                    serializer = serializers[serializer.substr(1)];
                else
                    serializer = linq.utils.lambda(serializer);
            }
        }
        if (!serializer) {
            if (value == null) {
                serializer = serializers.string;
            }
            else if (value instanceof Date) {
                serializer = serializers.date;
            }
            else if (value instanceof Array) {
                serializer = serializers.array;
            }
            else {
                var type = typeof value;
                switch (type) {
                    case 'boolean':
                    case 'number': serializer = serializers[type]; break;
                }
            }
        }
        return serializer || serializers.string;
    }

    function getDeserializer(value, deserializer) {
        if (deserializer) {
            if (typeof deserializer == 'string') {
                if (deserializer.charAt(0) == '#')
                    deserializer = deserializers[deserializer.substr(1)];
                else
                    deserializer = linq.utils.lambda(deserializer);
            }
        }
        if (!deserializer) {
            if (value == null) {
                deserializer = deserializers.string;
            }
            else if (value instanceof Date) {
                deserializer = deserializers.date;
            }
            else if (value instanceof Array) {
                deserializer = deserializers.array;
            }
            else {
                var type = typeof value;
                switch (type) {
                    case 'boolean':
                    case 'number': deserializer = deserializers[type]; break;
                }
            }
        }
        return deserializer || deserializers.string;
    }

    function subscribe(changeSet, observable, subscriptions) {
        for (var i = 0; i < changeSet.length; i++) {
            var change = changeSet[i];
            var args = [change.property, change.current, change];
            var removedSubscriptions = [];
            for (var j = 0; j < subscriptions.length; j++) {
                var subscription = subscriptions[j];
                var context = subscription.context || observable.model;
                if (subscription.condition && !subscription.condition.apply(context, args)) continue;
                var result = subscription.callback.apply(context, args);
                if (result === false) removedSubscriptions.push(j);
            }
            if (removedSubscriptions.length) {
                for (var k = 0; k < removedSubscriptions.length; k++) {
                    subscriptions.splice(removedSubscriptions[k], 1);
                }
            }
        }
    }

    function extractOptionValues(select, valueSelector) {
        var values = [];
        $(select).find('option:selected').each(function () {
            if ($(this).hasClass('option-caption')) return;
            var value = $(this).val();
            var source = $(this).data('source');
            if (typeof source == 'undefined' && value) {
                values.push(value);
            }
            else {
                values.push(valueSelector(source));
            }
        });
        return values;
    }

    function tryGetUrl(url, element) {
        if (!url) {
            var href = ($(element).attr('href') || '').toString();
            if (href) {
                if (href.indexOf('#') == -1 && href.toLowerCase().indexOf('javascript:') == -1) {
                    url = href;
                }
            }
            if (!url) {
                url = $(element).closest('form').attr('href') || location.href;
            }
        }
        return url;
    }

    function showLoading(element, target, loadingClass) {
        hideLoading(element);
        if (target) target = $(target).get(0);
        if (!target) return;

        var loading = $('<div class="ui-loading-indicator"><div class="ui-loading-indicator-wrap"><div class="ui-loading-indicator-inner"><span></span></div></div></div>');
        if (loadingClass) loading.addClass(loadingClass);
        $(element).data('__loadingIndicator', loading);
        var wrap = loading.find('.ui-loading-indicator-wrap');
        var width = $(element).outerWidth();
        var height = $(element).outerHeight();
        var offset = $(element).offset();
        loading.appendTo(document.body);
        loading.css({ overflow: 'hidden', position: 'absolute', left: offset.left, top: offset.top }).width(width).height(height);
        wrap.height(height);
        var wrapHeight = wrap.height();
        if (wrapHeight > height) {
            wrap.height(height - (wrapHeight - height));
        }
    }

    function hideLoading(element) {
        var loading = $(element).data('__loadingIndicator');
        if (loading) $(loading).remove();
    }

    bindingProviders.ajax = function (e) {
        var url = e.getMetadata(e.params.url, e.data);
        var active = e.getMetadata(e.params.active);
        if (e.change(url.property) || e.change(active.property) || (!active.property && active.value)) {
            var cache = e.cache();
            var ajax = cache.ajax;
            if (ajax) ajax.abort();
            var options = clone(e.params);
            var oldSuccess = options.success;
            if (options.data) options.data = clone(options.data, true);
            options.url = tryGetUrl(options.url, this);
            if (options.stringify) {
                if (typeof options.stringify == 'function') {
                    options.data = options.stringify(options.data);
                }
                else {
                    options.data = linq.toJSON(options.data);
                }
                if (!options.contentType) options.contentType = 'application/json; charset=utf-8;';
            }
            else if (options.postify) {
                options.data = postify(options.data);
                if (!options.contentType) options.contentType = 'application/x-www-form-urlencoded';
            }
            options.contentType += '; linq-model-request;';

            if (!options.context) options.context = e.element;

            options.success = function (d) {
                return processModelAjaxResult(d, function (data) {
                    if (oldSuccess && oldSuccess.apply(this, arguments) === false) return;
                    if ('defaultHtml' in options) {
                        if (/\s*/.test((data || '').toString())) {
                            data = options.defaultHtml;
                        }
                    }
                    $(e.element).html(data == null ? '' : data.toString());
                }, options.modelError);
            };
            var oldComplete = options.complete;
            options.complete = function () {
                cache.ajax = null;
                hideLoading(e.element);
                if (oldComplete) oldComplete.apply(this, arguments);
            };
            var oldError = options.error;
            options.error = function (xhr, status) {
                if (oldError && oldError.apply(this, arguments) === false) return;
                if (options.showError) $(e.element).text(status);
            };
            showLoading(this, options.loadingTarget || this, options.loadingClass);
            cache.ajax = $.ajax(options);
        }
    };

    bindingProviders.checked = {
        init: function (e) {
            e.cache('metadata', e.getMetadata(e.params.property, e.data));
            $(this).change(function () {
                var metadata = e.cache().metadata;
                if (metadata.property && metadata.model) {
                    metadata.model[metadata.property] = $(this).attr('checked');
                }
            });
        },
        update: function (e) {
            var metadata = e.getMetadata(e.params.property, e.data);
            e.cache('metadata', metadata);
            e.change(metadata.property, function () {
                $(this).attr('checked', !!metadata.value);
            }, true);
        }
    };

    bindingProviders.show = function (e) {
        var metadata = e.getMetadata(e.params.property, e.data);
        e.change(metadata.property, function () {
            $(this)[!!metadata.value ? 'show' : 'hide']();
        }, true);
    };

    bindingProviders.hide = function (e) {
        var metadata = e.getMetadata(e.params.property, e.data);
        e.change(metadata.property, function () {
            $(this)[!metadata.value ? 'show' : 'hide']();
        }, true);
    };

    bindingProviders.css = function (e) {
        $.each(e.params, function (name, value) {
            $(e.element).css(name, value);
        });
    };

    bindingProviders.attr = function (e) {
        $.each(e.params, function (name, value) {
            $(e.element).attr(name, value);
        });
    };

    bindingProviders.klass = function (e) {
        $.each(e.params, function (name, value) {
            if (!!value) {
                $(e.element).addClass(name);
            }
            else {
                $(e.element).removeClass(name);
            }
        });
    };

    $.each([
            'display',
            'left', 'top', 'width', 'height',
            'padding', 'paddingLeft', 'paddingTop', 'paddingRight', 'paddingBottom',
            'margin', 'marginLeft', 'marginTop', 'marginRight', 'marginBottom'
        ],
        function (index, property) {
            bindingProviders[property] = function (e) {
                var metadata = e.getMetadata(e.params.property, e.data);
                e.change(metadata.property, function () {
                    $(this).css(property, metadata.value);
                }, true);
            };
        });

    $.each([
            'click', 'change', 'mouseover', 'mouseout', 'blur', 'focus'
        ],
        function (index, property) {
            bindingProviders[property] = function (e) {
                var currentHandler = e.data;
                if (currentHandler) {
                    var cache = e.cache();
                    var oldHandler = cache[property + 'Handler'];
                    if (currentHandler !== oldHandler) {
                        if (oldHandler) $(this).unbind(property, oldHandler);
                        cache[property + 'Handler'] = currentHandler;
                        $(this).bind(property, currentHandler);
                    }
                }
            };
        });

    bindingProviders.disabled = function (e) {
        $(this).attr('disabled', !!e.getMetadata().value);
    };

    bindingProviders.enabled = function (e) {
        $(this).attr('disabled', !e.getMetadata().value);
    };

    bindingProviders.value = {
        init: function (e) {
            var metadata = e.getMetadata(e.params.property, e.data);
            var isMultiple = !!$(this).attr('multiple');
            var isArray = $.isArray(metadata.value);
            e.cache({
                serializer: getSerializer(metadata.value, e.params.type || e.params.serializer),
                deserializer: getDeserializer(metadata.value, e.params.type || e.params.deserializer)
            });

            if (isArray) {
                attachDependencyArray(metadata.value, e, function (bindingArgs, array) {
                    if (isMultiple) {
                        var valueComparer = linq.utils.lambda(bindingArgs.params.comparer) || settings.comparer;
                        $(this).find('option:selected').attr('selected', false);
                        $(this).find('option').each(function () {
                            if ($(this).hasClass('option-caption')) return;
                            var selectedValue = $(this).val();
                            var source = $(this).data('source');
                            if (source || !selectedValue) {
                                selectedValue = source;
                            }
                            for (var i = 0; i < array.length; i++) {
                                if (valueComparer(selectedValue, array[i])) {
                                    $(this).attr('selected', true);
                                    return;
                                }
                            }
                        });
                    }
                    else {
                        $(this).val(bindingArgs.cache().serializer(array));
                    }
                });
            }

            if (metadata.property) {
                var valueSelector = linq.utils.lambda(e.params.selector) || linq.delegates.returnFirst;
                $(this).bind(e.params.event || 'change', function () {
                    e.lock();
                    var propValue = e.model[metadata.property];
                    if (isMultiple) {
                        var selectedValues = extractOptionValues(this, valueSelector);
                        if (isArray) {
                            var i, j, origin, newValue, found;
                            // remove unsed values
                            for (i = propValue.length - 1; i >= 0; i--) {
                                origin = propValue[i];
                                found = false;
                                for (j = 0; j < selectedValues.length; j++) {
                                    if (settings.comparer(origin, selectedValues[j])) {
                                        found = true;
                                        break;
                                    }
                                }
                                if (!found) {
                                    propValue.splice(i, 1);
                                }
                            }
                            // insert new values
                            for (i = 0; i < selectedValues.length; i++) {
                                newValue = selectedValues[i];
                                found = false;
                                for (j = 0; j < propValue.length; j++) {
                                    if (settings.comparer(newValue, propValue[j])) {
                                        found = true;
                                        break;
                                    }
                                }
                                if (!found) {
                                    propValue.push(newValue);
                                }
                            }
                        }
                        else {
                            e.model[metadata.property] = e.cache().deserializer(selectedValues);
                        }
                    }
                    else {
                        var value = e.cache().deserializer($(this).val());
                        if (!isArray) {
                            e.model[metadata.property] = value;
                        }
                        else {
                            if ($.isArray(value)) {
                                value = value.slice(0);
                                value.unshift(propValue.length);
                                value.unshift(0);
                            }
                            else {
                                value = [0, propValue.length, value];
                            }
                            Array.prototype.splice.apply(propValue, value);
                        }
                    }
                });
            }
        },
        update: function (e) {
            var metadata = e.getMetadata(e.params.property, e.data);
            if ($.isArray(metadata.value)) {
                updateDependencyArray(metadata.value, e);
            }
            else {
                e.unlock(function () {
                    var newValue = e.cache().serializer(metadata.value);
                    $(this).val(newValue == null ? '' : newValue.toString());
                });
            }
        }
    };

    bindingProviders.each = {
        init: function (e) {
            var sourceMetadata = e.getMetadata(e.params.source, e.data);
            var templateSource = e.params.template;
            var template = getTemplate(templateSource, this);
            if (template.template) template.bindable = $(template.template).attr('bindable') != 'false';
            $(this).empty();
            var templateItems = [];
            attachDependencyArray(sourceMetadata.value, e, function (bindingArgs, array, property, newValue, notifyInfo) {
                var element = this;
                if (bindingArgs.params.template != templateSource) {
                    templateSource = bindingArgs.params.template;
                    template = getTemplate(templateSource);
                }
                if (!template.template) return;

                var removedItem;
                function createItem(value, prepend) {
                    var templateItem = $(template.template).tmpl({
                        data: value,
                        format: function (v, f) {
                            return serializers.string(v, f);
                        }
                    });
                    if (prepend) {
                        templateItem.prependTo(element);
                        templateItems.unshift(templateItem);
                    }
                    else {
                        templateItem.appendTo(element);
                        templateItems.push(templateItem);
                    }
                    if (template.bindable) {
                        linq.model(value, templateItem);
                    }
                }

                if (property == 'push') {
                    $.each(notifyInfo.items, function (index, value) {
                        createItem(value);
                    });
                }
                else if (property == 'unshift') {
                    notifyInfo.items.reverse();
                    $.each(notifyInfo.items, function (index, value) {
                        createItem(value, true);
                    });
                }
                else if (property == 'pop') {
                    removedItem = templateItems.pop();
                    if (removedItem) $(removedItem).remove();
                }
                else if (property == 'shift') {
                    removedItem = templateItems.shift();
                    if (removedItem) $(removedItem).remove();
                }
                else {
                    $(element).empty();
                    $.each(notifyInfo.items || array, function (index, value) {
                        createItem(value);
                    });
                }
            });
        },
        update: function (e) {
            var sourceMetadata = e.getMetadata(e.params.source, e.data);
            updateDependencyArray(sourceMetadata.value, e);
        }
    };

    bindingProviders.text = function (e) {
        var metadata = e.getMetadata(e.params.value, e.data);
        $(this).text(serializers.string(metadata.value, e.params.format));
    };

    bindingProviders.html = function (e) {
        var metadata = e.getMetadata();
        $(this).html(metadata.value == null ? '' : metadata.value.toString());
    };

    bindingProviders.unique = {
        init: function (e) {
            if (e.data === false) return;
            var properties = ((e.data === true ? e.params.property : e.data) || 'name').toString().replace(/ /g, '').split(',');
            $.each(properties, function (index, property) {
                if (!$(e.element).attr(property)) $(e.element).attr(property, linq.id(property + '-'));
            });
        }
    };

    function attachDependencyArray(array, bindingArgs, callback) {
        var dependencyElement = bindingArgs.element;
        var source = array;
        if (array == null) {
            array = [];
        }
        else if (!$.isArray(array)) {
            array = [array];
        }
        var data = {
            subscription: function (property, value, notifyInfo) {
                if (isElementRemoved(dependencyElement)) return false;
                return callback.call(dependencyElement, data.bindingArgs, data.array, property, value, notifyInfo);
            },
            first: true,
            updatable: source === array,
            source: source,
            array: array,
            bindingArgs: bindingArgs
        };
        if (data.updatable) {
            linq.model(data.array).subscribe(data.subscription);
        }
        $(dependencyElement).data('dependency-array-' + bindingArgs.provider, data);
    }

    function updateDependencyArray(newArray, bindingArgs, forceSubscribing) {
        var dependencyElement = bindingArgs.element;
        var data = $(dependencyElement).data('dependency-array-' + bindingArgs.provider);
        var newSource = newArray;
        if (newArray == null) {
            newArray = [];
        }
        else if (!$.isArray(newArray)) {
            newArray = [newArray];
        }

        if (data.source !== newSource) {
            if (data.updatable) {
                linq.model(data.array).unsubscribe(data.subscription);
            }
            data.source = newSource;
            data.array = newArray;
            data.updatable = newArray === newSource;
            data.bindingArgs = bindingArgs || data.bindingArgs;
            if (data.updatable) linq.model(data.array).subscribe(data.subscription);
            data.subscription.call(dependencyElement, '*', null, {});
        }
        else if (forceSubscribing || data.first) {
            data.subscription.call(dependencyElement, '*', null, {});
        }
        data.first = false;
    }

    function updateModel(model, newModel) {
        if (model && newModel) {
            if ($.isArray(model)) {
                newModel = $.isArray(newModel) ? newModel.slice(0) : [newModel];
                newModel.unshift(model.length);
                newModel.unshift(0);
                model.splice.apply(model, newModel);
            }
            else {
                $.each(newModel, function (name, value) {
                    if ($.isArray(value)) {
                        updateModel(model[name], value);
                    }
                    else if ($.isPlainObject(value)) {
                        if (value.__type === 'function') {
                            model[name] = eval('(' + value.body + ')');
                        }
                        else {
                            updateModel(model[name], value);
                        }
                    }
                    else {
                        model[name] = value;
                    }
                });
            }
        }
    }

    function convertModel(cons, model) {
        if (cons) {
            $.each(cons, function (name, converter) {
                if ($.isPlainObject(converter)) {
                    var prop = model[name];
                    if ($.isPlainObject(prop)) {
                        convertModel(converter, prop);
                    }
                }
                else {
                    converter = linq.utils.lambda(converter);
                    if (converter) {
                        model[name] = converter.call(model, model[name]);
                    }
                }
            });
        }
        return model;
    }

    function processModelAjaxResult(data, callback, modelError) {
        var parameters = data;
        if (data && data.__isModelResult) {
            if (data.error) {
                if (modelError) modelError(data.error);
                return;
            }
            else {
                switch (data.type) {
                    case 'redirect':
                        top.location.href = data.path;
                        return;
                    case 'open':
                        window.open(data.path, data.target || '_blank', data.features || '');
                        break;
                }
            }
            if (data.end) return;
            parameters = data.parameters;
        }
        if (callback) {
            return callback.call(this, parameters);
        }
    }

    function extractElementValues(elements, model) {
        var result = {};
        if (elements && model) {
            $.each(elements, function (name, element) {
                if ($.isPlainObject(element)) {
                    extractElementValues(element, result[name] = {});
                }
                else {
                    var values = [];
                    $(element).each(function () {
                        var value = $(this).val();
                        if ($(this).is('select')) {
                            if ($(this).hasClass('ui-send-all')) {
                                $(this).find('option').each(function () {
                                    values.push(value);
                                });
                            }
                            else {
                                if ($(this).attr('multiple')) {
                                    $(this).find('option:selected').each(function () {
                                        values.push($(this).val());
                                    });
                                }
                                else {
                                    if (!$(this).is(':checkbox, :radio') || $(this).is(':checked')) values.push(value);
                                }
                            }

                        }
                        else {
                            if (!$(this).is(':checkbox, :radio') || $(this).is(':checked')) values.push(value);
                        }
                    });
                    var isArray = /\[\]$/.test(name);
                    if (isArray) name = name.substr(0, name.length - 2);
                    result[name] = isArray ? values : values.join(',');
                }
            });
        }
        return result;
    }

    function postify(value) {
        var result = {};

        var buildResult = function (object, prefix) {
            for (var key in object) {

                var postKey = isFinite(key)
                    ? (prefix != "" ? prefix : "") + "[" + key + "]"
                    : (prefix != "" ? prefix + "." : "") + key;
                var v = object[key];
                switch (typeof (v)) {
                    case "number": case "string": case "boolean":
                        result[postKey] = v;
                        break;

                    case "object":
                        if (v == null) {
                            result[postKey] = v;
                        }
                        else if (v.toUTCString)
                            result[postKey] = v.toUTCString().replace("UTC", "GMT");
                        else {
                            buildResult(v, postKey != "" ? postKey : key);
                        }
                }
            }
        };

        buildResult(value, "");

        return result;
    }

    bindingProviders.noSubmit = {
        init: function (e) {
            if (e.data) {
                $(this).submit(function (ee) {
                    ee.stopPropagation();
                    return false;
                });
            }
        }
    };

    bindingProviders.send = {
        init: function (e) {
            var validate = e.params.validate;

            $(this).bind(e.params.on || 'click', function () {
                if (validate) {
                    if (typeof validate == 'boolean') {
                        var form = $(this).closest('form');
                        form.validate();
                        if (!form.valid()) return;
                    }
                    else if (typeof validate == 'function') {
                        if (!validate.call(this, e.model)) return;
                    }
                }
                var request = e.cache().request;
                if (!request) return;
                request = clone(request);
                request.url = tryGetUrl(request.url, this);

                var model = request.model || e.model;
                if (request.data == null) request.data = model;
                if (request.data) request.data = linq.model.clone(request.data, true);
                if (request.preProcess) request.data = request.preProcess(request.data);
                if (!request.type) {
                    request.type = 'post';
                }
                if (!request.dataType) request.dataType = 'json';
                if (request.stringify) {
                    if (typeof request.stringify == 'function') {
                        request.data = request.stringify(request.data);
                    }
                    else {
                        request.data = linq.toJSON(request.data);
                    }
                    if (!request.contentType) request.contentType = 'application/json; charset=utf-8;';
                }
                else if (request.postify) {
                    request.data = postify(request.data);
                    if (!request.contentType) request.contentType = 'application/x-www-form-urlencoded';
                }
                request.contentType += '; linq-model-request;';
                if (!request.context) request.context = model;
                var isJson = request.dataType == 'json';
                request.success = function (ajaxResult) {
                    return processModelAjaxResult(ajaxResult, function (data) {
                        if (request.updating && request.updating.apply(this, arguments) === false) return;
                        if (request.update !== false) {
                            if (typeof request.update == 'function') {
                                request.update(data, model);
                            }
                            else {
                                if (isJson) {
                                    if (data) {
                                        var modelSelector = linq.utils.lambda(request.selector) || linq.delegates.returnFirst;
                                        var newModel = convertModel(request.converters, modelSelector(data));
                                        updateModel(model, newModel);
                                    }
                                }
                                else {
                                    $(request.update).html(data);
                                }
                            }
                        }
                        if (request.updated) request.updated.apply(this, arguments);
                    }, request.modelError);
                };
                request.complete = function () {
                    hideLoading(e.element);
                };
                showLoading(e.element, request.loadingTarget || this, request.loadingClass);
                $.ajax(request);

                return request.result;
            });
        },
        update: function (e) {
            var cache = e.cache();
            var params = e.params;
            if (typeof params == 'string') {
                params = { url: params };
            }
            if (!cache.request) cache.request = {};
            $.extend(cache.request, params);
        }
    };

    bindingProviders.assoc = function (e) {
        var selector = e.getMetadata(e.params.selector, e.data);
        e.change(selector.property, function () {
            var context = e.params.context || e.element;
            var next;
            if (selector.value && typeof selector.value !== 'boolean') {
                next = $(selector, context).first();
            }
            else {
                next = $(context).next('input, select, textarea').first();
                if (!next.length) next = $(e.element).next().find('input, select, textarea').first();
                if (!next.length) next = $(e.element).parent().next().find('input, select, textarea').first();
            }
            var id = null;
            if (next.length) {
                id = next.attr('id');
                if (!id) next.attr('id', id = linq.id('element-'));
            }
            $(e.element).attr('for', id || '');
        }, true);
    };

    bindingProviders.mask = function (e) {
        var maskMetadata = e.getMetadata();
        if (maskMetadata.value) {
            e.change(maskMetadata.property, function () {
                $(this).mask(maskMetadata.value);
            }, true);
        }
    };

    bindingProviders.rules = function (e) {
        var rules = e.params;
        var cache = e.cache();
        var messageTemplate = e.params.message;
        delete e.params.message;
        var name = $(this).attr('name');
        if (!name) $(this).attr('name', name = linq.id('input-'));
        if (!cache.addRules || rules.dynamic) {
            $(this).rules('add', rules);
            cache.addRules = true;
        }
        if (!cache.createMessage) {
            cache.createMessage = true;
            if (typeof messageTemplate == 'function') {
                messageTemplate.call(this, e.model);
            }
            else if (messageTemplate) {

                if (typeof messageTemplate == 'boolean' || messageTemplate == 'before' || messageTemplate == 'after' || messageTemplate == 'prepend' || messageTemplate == 'append') {
                    var errorElement = $('<span data-valmsg-replace="true"></span>').attr('data-valmsg-for', name);
                    switch (messageTemplate) {
                        case 'append': $(this).parent().append(errorElement); break;
                        case 'prepend': $(this).parent().prepend(errorElement); break;
                        case 'before': $(this).before(errorElement); break;
                        default: $(this).after(errorElement); break;
                    }
                }
                else {
                    $(this).after($(messageTemplate).attr('data-valmsg-for', name));
                }
            }
        }
    };

    bindingProviders.options = {
        init: function (e) {
            var sourceMetadata = e.getMetadata(e.params.source, e.data);
            var captionText = e.params.caption;
            var captionElement = $(this).find('.option-caption').get(0);
            $(this).empty();
            if (captionText) {
                if (captionElement) {
                    $(captionElement).text(captionText);
                }
                else {
                    captionElement = $('<option class="option-caption"/>')
                        .val('')
                        .text(captionText);
                }
            }
            if (captionElement) $(this).append(captionElement);

            attachDependencyArray(sourceMetadata.value, e, function (bindingArgs, array, property, newValue, notifyInfo) {
                if (bindingArgs.params.caption !== captionText) {
                    captionText = bindingArgs.params.caption;
                    if (captionText) {
                        if (captionElement) {
                            $(captionElement).text();
                        }
                        else {
                            captionElement = $('<option class="option-caption"/>')
                                .val('')
                                .text(captionText)
                                .prependTo(this);
                        }
                    }
                    else {
                        if (captionElement) $(captionElement).remove();
                        captionElement = null;
                    }
                }

                var textSelector = linq.utils.lambda(bindingArgs.params.text) || linq.delegates.returnFirst;
                var valueSelector = linq.utils.lambda(bindingArgs.params.value) || linq.delegates.returnFirst;
                var storeSelector = linq.utils.lambda(bindingArgs.params.store) || linq.delegates.returnFirst;
                var element = this;
                var optionToRemove, i, optionItems;

                function createOption(value, prepend) {
                    var option = $('<option class="option-item"/>')
                        .data('source', storeSelector(value))
                        .val(valueSelector(value))
                        .text(textSelector(value));
                    if (prepend) {
                        if (captionElement) {
                            $(captionElement).after(option);
                        }
                        else {
                            option.prependTo(element);
                        }
                    }
                    else {
                        option.appendTo(element);
                    }
                }

                if (property == 'push') {
                    $.each(notifyInfo.items, function (index, value) {
                        createOption(value);
                    });
                }
                else if (property == 'unshift') {
                    notifyInfo.items.reverse();
                    $.each(notifyInfo.items, function (index, value) {
                        createOption(value, true);
                    });
                }
                else if (property == 'pop') {
                    optionItems = $(element).find('.option-item').toArray();
                    for (i = 0; i < notifyInfo.items.length; i++) {
                        optionToRemove = optionItems.pop();
                        if (optionToRemove && $(optionToRemove).data('source') == notifyInfo.items[i]) $(optionToRemove).remove();
                    }
                }
                else if (property == 'shift') {
                    optionItems = $(element).find('.option-item').toArray();
                    for (i = 0; i < notifyInfo.items.length; i++) {
                        optionToRemove = optionItems.shift();
                        if (optionToRemove && $(optionToRemove).data('source') == notifyInfo.items[i]) $(optionToRemove).remove();
                    }
                }
                else {

                    var selectedValue = null;
                    var selectedValues = [];
                    var isMultiple = !!$(element).attr('multiple');
                    if (isMultiple) {
                        $(element).find('.option-item:selected').each(function () {
                            selectedValues.push({ value: $(this).data('source') });
                        });
                    }
                    else {
                        if (element.selectedIndex != -1) {
                            $(element).find('option-item:selected').each(function () {
                                selectedValue = { value: $(this).data('source') };
                            });
                        }
                    }

                    // clear options
                    $(element).empty();

                    if (captionElement) $(element).append(captionElement);

                    $.each(notifyInfo.items || array, function (index, value) {
                        createOption(value);
                    });
                    // restore selection
                    if (isMultiple) {
                        selectedValues = linq(selectedValues);
                        $(element).find('.option-item').each(function () {
                            var data = $(this).data('source');
                            $(this).attr('selected', selectedValues.any(function (x) {
                                return x.value === data;
                            }));
                        });
                    }
                    else {
                        if (selectedValue) {
                            $(element).find('.option-item').each(function () {
                                var data = $(this).data('source');
                                var found = data === selectedValue.value;
                                $(this).attr('selected', found);
                                if (found) return false;
                            });
                        }
                    }
                }
            });
        },
        update: function (e) {
            var sourceMetadata = e.getMetadata(e.params.source, e.data);
            updateDependencyArray(sourceMetadata.value, e);
        }
    };

    settings.comparer = defaultComparer;
    $.extend(linq.model, {
        converters: converters,
        serializers: serializers,
        deserializers: deserializers,
        types: types,
        clone: clone,
        providers: bindingProviders
    });

    timerId = setTimeout(function () {
        clearTimeout(timerId);
        var obs = observables.slice(0);
        for (var i = 0; i < obs.length; i++) {
            var ob = obs[i];
            var subscriptions = ob.subscriptions;
            if (!subscriptions || !subscriptions.length) continue;
            var changeSet = ob.getChangeSet();
            if (changeSet && changeSet.length) subscribe(changeSet, ob, subscriptions);
        }
        timerId = setTimeout(arguments.callee, timerDelay);
    }, timerDelay);
})(window.linq, window.jQuery, window.Globalize);